Skip to content

Conversation

JustinStitt
Copy link
Contributor

@JustinStitt JustinStitt commented Jul 15, 2025

Introduce OverflowBehaviorType (OBT), a new type attribute in Clang that provides developers with fine-grained control over the overflow behavior of integer types. This feature allows for a more nuanced approach to integer safety, achieving better granularity than global compiler flags like -fwrapv and -ftrapv. Type specifiers are also available via __ob_wrap and __ob_trap.

These can be applied to integer types (both signed and unsigned) as well as typedef declarations, where the behavior is one of the following:

  • wrap: Guarantees that arithmetic operations on the type will wrap on overflow, similar to -fwrapv. This suppresses UBSan's integer overflow checks for the attributed type and prevents eager compiler optimizations.
  • trap: Enforces overflow checking for the type, even when global flags like -fwrapv would otherwise suppress it.

A key aspect of this feature is its interaction with existing mechanisms. OverflowBehaviorType takes precedence over global flags and, notably, over entries in the Sanitizer Special Case List (SSCL). This allows developers to "allowlist" critical types for overflow instrumentation, even if they are disabled by a broad rule in an SSCL.

See the docs and tests for examples and more info.

RFCs

RFC v2

RFC v1

CCs (because of your participation in RFCs or previous implementations)

@kees @AaronBallman @vitalybuka @melver @efriedma-quic

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang:codegen IR generation bugs: mangling, exceptions, etc. clang:as-a-library libclang and C++ API debuginfo labels Jul 15, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 15, 2025

@llvm/pr-subscribers-lldb
@llvm/pr-subscribers-debuginfo
@llvm/pr-subscribers-clang-codegen
@llvm/pr-subscribers-clang-modules
@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-driver

Author: Justin Stitt (JustinStitt)

Changes

Introduce OverflowBehaviorType (OBT), a new type attribute in Clang that provides developers with fine-grained control over the overflow behavior of integer types. This feature allows for a more nuanced approach to integer safety, achieving better granularity than global compiler flags like -fwrapv and -ftrapv.

The new __attribute__((overflow_behavior(behavior))) can be applied to integer types (both signed and unsigned) as well as typedef declarations, where behavior is one of the following:

  • wrap: Guarantees that arithmetic operations on the type will wrap on overflow, similar to -fwrapv. This suppresses UBSan's integer overflow checks for the attributed type.
  • no_wrap: Enforces overflow checking for the type, even when global flags like -fwrapv would otherwise suppress it.

A key aspect of this feature is its interaction with existing mechanisms. OverflowBehaviorType takes precedence over global flags and, notably, over entries in the Sanitizer Special Case List (SSCL). This allows developers to "allowlist" critical types for overflow instrumentation, even if they are disabled by a broad rule in an SSCL.

This implementation also includes specific promotion rules to preserve the intended overflow behavior within expressions and new diagnostics to warn against accidentally discarding the attribute.

See the docs and tests for examples and more info.

RFCs

RFC v2

RFC v1

CCs (because of your participation in RFCs or previous implementations)

@kees @AaronBallman @vitalybuka @melver @efriedma-quic


Patch is 138.97 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/148914.diff

66 Files Affected:

  • (added) clang/docs/OverflowBehaviorTypes.rst (+269)
  • (modified) clang/docs/ReleaseNotes.rst (+6)
  • (modified) clang/docs/SanitizerSpecialCaseList.rst (+34)
  • (modified) clang/docs/UndefinedBehaviorSanitizer.rst (+22)
  • (modified) clang/docs/index.rst (+1)
  • (modified) clang/include/clang/AST/ASTContext.h (+20)
  • (modified) clang/include/clang/AST/ASTNodeTraverser.h (+3)
  • (modified) clang/include/clang/AST/Expr.h (+8)
  • (modified) clang/include/clang/AST/PropertiesBase.td (+2)
  • (modified) clang/include/clang/AST/RecursiveASTVisitor.h (+6)
  • (modified) clang/include/clang/AST/Stmt.h (+3)
  • (modified) clang/include/clang/AST/Type.h (+74-1)
  • (modified) clang/include/clang/AST/TypeLoc.h (+22)
  • (modified) clang/include/clang/AST/TypeProperties.td (+13)
  • (modified) clang/include/clang/Basic/Attr.td (+7)
  • (modified) clang/include/clang/Basic/DiagnosticGroups.td (+11)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+24)
  • (modified) clang/include/clang/Basic/LangOptions.def (+2)
  • (modified) clang/include/clang/Basic/LangOptions.h (+19)
  • (modified) clang/include/clang/Basic/TypeNodes.td (+1)
  • (modified) clang/include/clang/Driver/Options.td (+6)
  • (modified) clang/include/clang/Sema/Sema.h (+12)
  • (modified) clang/include/clang/Serialization/ASTRecordReader.h (+4)
  • (modified) clang/include/clang/Serialization/ASTRecordWriter.h (+2)
  • (modified) clang/include/clang/Serialization/TypeBitCodes.def (+1)
  • (modified) clang/lib/AST/ASTContext.cpp (+154)
  • (modified) clang/lib/AST/ASTDiagnostic.cpp (+7)
  • (modified) clang/lib/AST/ASTImporter.cpp (+12)
  • (modified) clang/lib/AST/ASTStructuralEquivalence.cpp (+7)
  • (modified) clang/lib/AST/ExprConstant.cpp (+4-2)
  • (modified) clang/lib/AST/FormatString.cpp (+4)
  • (modified) clang/lib/AST/ItaniumMangle.cpp (+6)
  • (modified) clang/lib/AST/MicrosoftMangle.cpp (+5)
  • (modified) clang/lib/AST/Type.cpp (+80-1)
  • (modified) clang/lib/AST/TypeLoc.cpp (+8)
  • (modified) clang/lib/AST/TypePrinter.cpp (+22)
  • (modified) clang/lib/CodeGen/CGDebugInfo.cpp (+7)
  • (modified) clang/lib/CodeGen/CGDebugInfo.h (+1)
  • (modified) clang/lib/CodeGen/CGExprScalar.cpp (+172-108)
  • (modified) clang/lib/CodeGen/CodeGenFunction.cpp (+2)
  • (modified) clang/lib/CodeGen/CodeGenTypes.cpp (+4)
  • (modified) clang/lib/CodeGen/ItaniumCXXABI.cpp (+5)
  • (modified) clang/lib/Driver/ToolChains/Clang.cpp (+3)
  • (modified) clang/lib/Sema/SemaChecking.cpp (+32)
  • (modified) clang/lib/Sema/SemaDecl.cpp (+7)
  • (modified) clang/lib/Sema/SemaExpr.cpp (+73-11)
  • (modified) clang/lib/Sema/SemaLookup.cpp (+2)
  • (modified) clang/lib/Sema/SemaOverload.cpp (+97-2)
  • (modified) clang/lib/Sema/SemaTemplate.cpp (+5)
  • (modified) clang/lib/Sema/SemaTemplateDeduction.cpp (+2)
  • (modified) clang/lib/Sema/SemaType.cpp (+85)
  • (modified) clang/lib/Sema/TreeTransform.h (+7)
  • (modified) clang/lib/Serialization/ASTReader.cpp (+4)
  • (modified) clang/lib/Serialization/ASTWriter.cpp (+4)
  • (added) clang/test/CodeGen/overflow-behavior-types-extensions.c (+44)
  • (added) clang/test/CodeGen/overflow-behavior-types-operators.cpp (+70)
  • (added) clang/test/CodeGen/overflow-behavior-types-promotions.cpp (+50)
  • (added) clang/test/CodeGen/overflow-behavior-types-scl.c (+43)
  • (added) clang/test/CodeGen/overflow-behavior-types.c (+148)
  • (added) clang/test/CodeGen/overflow-behavior-types.cpp (+68)
  • (modified) clang/test/Misc/pragma-attribute-supported-attributes-list.test (+1)
  • (added) clang/test/Sema/attr-overflow-behavior-constexpr.cpp (+44)
  • (added) clang/test/Sema/attr-overflow-behavior-off.c (+3)
  • (added) clang/test/Sema/attr-overflow-behavior.c (+41)
  • (added) clang/test/Sema/attr-overflow-behavior.cpp (+115)
  • (modified) clang/tools/libclang/CIndex.cpp (+4)
diff --git a/clang/docs/OverflowBehaviorTypes.rst b/clang/docs/OverflowBehaviorTypes.rst
new file mode 100644
index 0000000000000..18e337e181e8a
--- /dev/null
+++ b/clang/docs/OverflowBehaviorTypes.rst
@@ -0,0 +1,269 @@
+=====================
+OverflowBehaviorTypes
+=====================
+
+.. contents::
+   :local:
+
+Introduction
+============
+
+Clang provides a type attribute that allows developers to have fine-grained control
+over the overflow behavior of integer types. The ``overflow_behavior``
+attribute can be used to specify how arithmetic operations on a given integer
+type should behave upon overflow. This is particularly useful for projects that
+need to balance performance and safety, allowing developers to enable or
+disable overflow checks for specific types.
+
+The attribute can be enabled using the compiler option
+``-foverflow-behavior-types``.
+
+The attribute syntax is as follows:
+
+.. code-block:: c++
+
+  __attribute__((overflow_behavior(behavior)))
+
+Where ``behavior`` can be one of the following:
+
+* ``wrap``: Specifies that arithmetic operations on the integer type should
+  wrap on overflow. This is equivalent to the behavior of ``-fwrapv``, but it
+  applies only to the attributed type and may be used with both signed and
+  unsigned types. When this is enabled, UBSan's integer overflow and integer
+  truncation checks (``signed-integer-overflow``,
+  ``unsigned-integer-overflow``, ``implicit-signed-integer-truncation``, and
+  ``implicit-unsigned-integer-truncation``) are suppressed for the attributed
+  type.
+
+* ``no_wrap``: Specifies that arithmetic operations on the integer type should
+  be checked for overflow. When using the ``signed-integer-overflow`` sanitizer
+  or when using ``-ftrapv`` alongside a signed type, this is the default
+  behavior. Using this, one may enforce overflow checks for a type even when
+  ``-fwrapv`` is enabled globally.
+
+This attribute can be applied to ``typedef`` declarations and to integer types directly.
+
+Examples
+========
+
+Here is an example of how to use the ``overflow_behavior`` attribute with a ``typedef``:
+
+.. code-block:: c++
+
+  typedef unsigned int __attribute__((overflow_behavior(no_wrap))) non_wrapping_uint;
+
+  non_wrapping_uint add_one(non_wrapping_uint a) {
+    return a + 1; // Overflow is checked for this operation.
+  }
+
+Here is an example of how to use the ``overflow_behavior`` attribute with a type directly:
+
+.. code-block:: c++
+
+  int mul_alot(int n) {
+    int __attribute__((overflow_behavior(wrap))) a = n;
+    return a * 1337; // Potential overflow is not checked and is well-defined
+  }
+
+"Well-defined" overflow is consistent with two's complement wrap-around
+semantics and won't be removed via eager compiler optimizations (like some
+undefined behavior might).
+
+Overflow behavior types are implicitly convertible to and from built-in
+integral types.
+
+Note that C++ overload set formation rules treat promotions to and from
+overflow behavior types the same as normal integral promotions and conversions.
+
+Interaction with Command-Line Flags and Sanitizer Special Case Lists
+====================================================================
+
+The ``overflow_behavior`` attribute interacts with sanitizers, ``-ftrapv``,
+``-fwrapv``, and Sanitizer Special Case Lists (SSCL) by wholly overriding these
+global flags. The following table summarizes the interactions:
+
+.. list-table:: Overflow Behavior Precedence
+   :widths: 15 15 15 15 20 15
+   :header-rows: 1
+
+   * - Behavior
+     - Default(No Flags)
+     - -ftrapv
+     - -fwrapv
+     - Sanitizers
+     - SSCL
+   * - ``overflow_behavior(wrap)``
+     - Wraps
+     - No trap
+     - Wraps
+     - No report
+     - Overrides SSCL
+   * - ``overflow_behavior(no_wrap)``
+     - Traps
+     - Traps
+     - Traps
+     - Reports
+     - Overrides SSCL
+
+It is important to note the distinction between signed and unsigned types. For
+unsigned integers, which wrap on overflow by default, ``overflow_behavior(no_wrap)``
+is particularly useful for enabling overflow checks. For signed integers, whose
+overflow behavior is undefined by default, ``overflow_behavior(wrap)`` provides
+a guaranteed wrapping behavior.
+
+The ``overflow_behavior`` attribute can be used to override the behavior of
+entries from a :doc:`SanitizerSpecialCaseList`. This is useful for allowlisting
+specific types into overflow instrumentation.
+
+Promotion Rules
+===============
+
+The promotion rules for overflow behavior types are designed to preserve the
+specified overflow behavior throughout an arithmetic expression. They differ
+from standard C/C++ integer promotions but in a predictable way, similar to
+how ``_Complex`` and ``_BitInt`` have their own promotion rules.
+
+* **OBT and Standard Integer Type**: In an operation involving an overflow
+  behavior type (OBT) and a standard integer type, the result will have the
+  type of the OBT, including its overflow behavior, sign, and bit-width. The
+  standard integer type is implicitly converted to match the OBT.
+
+  .. code-block:: c++
+
+    typedef char __attribute__((overflow_behavior(no_wrap))) no_wrap_char;
+    // The result of this expression is no_wrap_char.
+    no_wrap_char c;
+    unsigned long ul;
+    auto result = c + ul;
+
+* **Two OBTs of the Same Kind**: When an operation involves two OBTs of the
+  same kind (e.g., both ``wrap``), the result will have the larger of the two
+  bit-widths. If the bit-widths are the same, an unsigned type is favored over
+  a signed one.
+
+  .. code-block:: c++
+
+    typedef unsigned char __attribute__((overflow_behavior(wrap))) u8_wrap;
+    typedef unsigned short __attribute__((overflow_behavior(wrap))) u16_wrap;
+    // The result of this expression is u16_wrap.
+    u8_wrap a;
+    u16_wrap b;
+    auto result = a + b;
+
+* **Two OBTs of Different Kinds**: In an operation between a ``wrap`` and a
+  ``no_wrap`` type, a ``no_wrap`` is produced. It is recommended to avoid such
+  operations, as Clang may emit a warning for such cases in the future.
+  Regardless, the resulting type matches the bit-width, sign and behavior of
+  the ``no_wrap`` type.
+
+Diagnostics
+===========
+
+Clang provides diagnostics to help developers manage overflow behavior types.
+
+-Wimplicitly-discarded-overflow-behavior
+----------------------------------------
+
+This warning is issued when an overflow behavior type is implicitly converted
+to a standard integer type, which may lead to the loss of the specified
+overflow behavior.
+
+.. code-block:: c++
+
+  typedef int __attribute__((overflow_behavior(wrap))) wrapping_int;
+
+  void some_function(int);
+
+  void another_function(wrapping_int w) {
+    some_function(w); // warning: implicit conversion from 'wrapping_int' to
+                      // 'int' discards overflow behavior
+  }
+
+To fix this, you can explicitly cast the overflow behavior type to a standard
+integer type.
+
+.. code-block:: c++
+
+  typedef int __attribute__((overflow_behavior(wrap))) wrapping_int;
+
+  void some_function(int);
+
+  void another_function(wrapping_int w) {
+    some_function(static_cast<int>(w)); // OK
+  }
+
+This warning acts as a group that includes
+``-Wimplicitly-discarded-overflow-behavior-pedantic`` and
+``-Wimplicitly-discarded-overflow-behavior-assignment``.
+
+-Wimplicitly-discarded-overflow-behavior-pedantic
+-------------------------------------------------
+
+A less severe version of the warning, ``-Wimplicitly-discarded-overflow-behavior-pedantic``,
+is issued for implicit conversions from an unsigned wrapping type to a standard
+unsigned integer type. This is considered less problematic because both types
+have well-defined wrapping behavior, but the conversion still discards the
+explicit ``overflow_behavior`` attribute.
+
+.. code-block:: c++
+
+  typedef unsigned int __attribute__((overflow_behavior(wrap))) wrapping_uint;
+
+  void some_function(unsigned int);
+
+  void another_function(wrapping_uint w) {
+    some_function(w); // warning: implicit conversion from 'wrapping_uint' to
+                      // 'unsigned int' discards overflow behavior
+                      // [-Wimplicitly-discarded-overflow-behavior-pedantic]
+  }
+
+-Wimplicitly-discarded-overflow-behavior-assignment
+---------------------------------------------------
+
+This warning is issued when an overflow behavior type is implicitly converted
+to a standard integer type as part of an assignment, which may lead to the
+loss of the specified overflow behavior. This is a more specific version of
+the ``-Wimplicitly-discarded-overflow-behavior`` warning, and it is off by
+default.
+
+.. code-block:: c++
+
+  typedef int __attribute__((overflow_behavior(wrap))) wrapping_int;
+
+  void some_function() {
+    wrapping_int w = 1;
+    int i = w; // warning: implicit conversion from 'wrapping_int' to 'int'
+               // discards overflow behavior
+               // [-Wimplicitly-discarded-overflow-behavior-assignment]
+  }
+
+To fix this, you can explicitly cast the overflow behavior type to a standard
+integer type.
+
+.. code-block:: c++
+
+  typedef int __attribute__((overflow_behavior(wrap))) wrapping_int;
+
+  void some_function() {
+    wrapping_int w = 1;
+    int i = static_cast<int>(w); // OK
+    int j = (int)w; // C-style OK
+  }
+
+
+-Woverflow-behavior-attribute-ignored
+-------------------------------------
+
+This warning is issued when the ``overflow_behavior`` attribute is applied to
+a type that is not an integer type.
+
+.. code-block:: c++
+
+  typedef float __attribute__((overflow_behavior(wrap))) wrapping_float;
+  // warning: 'overflow_behavior' attribute only applies to integer types;
+  // attribute is ignored [-Woverflow-behavior-attribute-ignored]
+
+  typedef struct S { int i; } __attribute__((overflow_behavior(wrap))) S_t;
+  // warning: 'overflow_behavior' attribute only applies to integer types;
+  // attribute is ignored [-Woverflow-behavior-attribute-ignored]
+
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 970825c98fec1..0c9139cb252d2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -378,6 +378,8 @@ New Compiler Flags
 
 - New options ``-g[no-]key-instructions`` added, disabled by default. Reduces jumpiness of debug stepping for optimized code in some debuggers (not LLDB at this time). Not recommended for use without optimizations. DWARF only. Note both the positive and negative flags imply ``-g``.
 
+- New option ``-foverflow-behavior-types`` added to enable parsing of the ``overflow_behavior`` type attribute.
+
 Deprecated Compiler Flags
 -------------------------
 
@@ -478,6 +480,10 @@ related warnings within the method body.
 - Clang will print the "reason" string argument passed on to
   ``[[clang::warn_unused_result("reason")]]`` as part of the warning diagnostic.
 
+- Introduced a new type attribute ``__attribute__((overflow_behavior))`` which
+  currently accepts either ``wrap`` or ``no_wrap`` as an argument, enabling
+  type-level control over overflow behavior.
+
 Improvements to Clang's diagnostics
 -----------------------------------
 
diff --git a/clang/docs/SanitizerSpecialCaseList.rst b/clang/docs/SanitizerSpecialCaseList.rst
index 2c50778d0f491..f4c9274b89b68 100644
--- a/clang/docs/SanitizerSpecialCaseList.rst
+++ b/clang/docs/SanitizerSpecialCaseList.rst
@@ -133,6 +133,40 @@ precedence. Here are a few examples.
   fun:*bar
   fun:bad_bar=sanitize
 
+Interaction with Overflow Behavior Types
+----------------------------------------
+
+The ``overflow_behavior`` attribute provides a more granular, source-level
+control that takes precedence over the Sanitizer Special Case List. If a type
+is given an ``overflow_behavior`` attribute, it will override any matching
+``type:`` entry in a special case list.
+
+This allows developers to enforce a specific overflow behavior for a critical
+type, even if a broader rule in the special case list would otherwise disable
+instrumentation for it.
+
+.. code-block:: bash
+
+  $ cat ignorelist.txt
+  # Disable signed overflow checks for all types by default.
+  [signed-integer-overflow]
+  type:*
+
+  $ cat foo.c
+  // Force 'critical_type' to always have overflow checks,
+  // overriding the ignorelist.
+  typedef int __attribute__((overflow_behavior(no_wrap))) critical_type;
+
+  void foo(int x) {
+    critical_type a = x;
+    a++; // Overflow is checked here due to the 'no_wrap' attribute.
+
+    int b = x;
+    b++; // Overflow is NOT checked here due to the ignorelist.
+  }
+
+For more details on overflow behavior types, see :doc:`OverflowBehaviorTypes`.
+
 Format
 ======
 
diff --git a/clang/docs/UndefinedBehaviorSanitizer.rst b/clang/docs/UndefinedBehaviorSanitizer.rst
index 0a2d833783e57..f98eda1e9399c 100644
--- a/clang/docs/UndefinedBehaviorSanitizer.rst
+++ b/clang/docs/UndefinedBehaviorSanitizer.rst
@@ -380,6 +380,28 @@ This attribute may not be
 supported by other compilers, so consider using it together with
 ``#if defined(__clang__)``.
 
+Disabling Overflow Instrumentation with ``__attribute__((overflow_behavior(wrap)))``
+------------------------------------------------------------------------------------
+
+For more fine-grained control over how integer overflow is handled, you can use
+the ``__attribute__((overflow_behavior(wrap)))`` attribute. This attribute can
+be applied to ``typedef`` declarations and integer types to specify that
+arithmetic operations on that type should wrap on overflow. This can be used to
+disable overflow sanitization for specific types, while leaving it enabled for
+all other types.
+
+For more information, see :doc:`OverflowBehaviorTypes`.
+
+Enforcing Overflow Instrumentation with ``__attribute__((overflow_behavior(no_wrap)))``
+---------------------------------------------------------------------------------------
+
+Conversely, you can use ``__attribute__((overflow_behavior(no_wrap)))`` to
+enforce overflow checks for a specific type, even when ``-fwrapv`` is enabled
+globally. This is useful for ensuring that critical calculations are always
+checked for overflow, regardless of the global compiler settings.
+
+For more information, see :doc:`OverflowBehaviorTypes`.
+
 Suppressing Errors in Recompiled Code (Ignorelist)
 --------------------------------------------------
 
diff --git a/clang/docs/index.rst b/clang/docs/index.rst
index 6c792af66a62c..3205709aa395a 100644
--- a/clang/docs/index.rst
+++ b/clang/docs/index.rst
@@ -40,6 +40,7 @@ Using Clang as a Compiler
    SanitizerCoverage
    SanitizerStats
    SanitizerSpecialCaseList
+   OverflowBehaviorTypes
    BoundsSafety
    BoundsSafetyAdoptionGuide
    BoundsSafetyImplPlans
diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index 2b9cd035623cc..dbfc8df6fba5e 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -14,6 +14,7 @@
 #ifndef LLVM_CLANG_AST_ASTCONTEXT_H
 #define LLVM_CLANG_AST_ASTCONTEXT_H
 
+#include "Type.h"
 #include "clang/AST/ASTFwd.h"
 #include "clang/AST/CanonicalType.h"
 #include "clang/AST/CommentCommandTraits.h"
@@ -259,6 +260,7 @@ class ASTContext : public RefCountedBase<ASTContext> {
   mutable llvm::ContextualFoldingSet<DependentBitIntType, ASTContext &>
       DependentBitIntTypes;
   mutable llvm::FoldingSet<BTFTagAttributedType> BTFTagAttributedTypes;
+  mutable llvm::FoldingSet<OverflowBehaviorType> OverflowBehaviorTypes;
   llvm::FoldingSet<HLSLAttributedResourceType> HLSLAttributedResourceTypes;
   llvm::FoldingSet<HLSLInlineSpirvType> HLSLInlineSpirvTypes;
 
@@ -899,6 +901,8 @@ class ASTContext : public RefCountedBase<ASTContext> {
   bool isTypeIgnoredBySanitizer(const SanitizerMask &Mask,
                                 const QualType &Ty) const;
 
+  bool isUnaryOverflowPatternExcluded(const UnaryOperator *UO);
+
   const XRayFunctionFilter &getXRayFilter() const {
     return *XRayFilter;
   }
@@ -999,6 +1003,15 @@ class ASTContext : public RefCountedBase<ASTContext> {
   comments::FullComment *getCommentForDecl(const Decl *D,
                                            const Preprocessor *PP) const;
 
+  /// Attempts to merge two types that may be OverflowBehaviorTypes.
+  ///
+  /// \returns A QualType if the types were handled, std::nullopt otherwise.
+  /// A null QualType indicates an incompatible merge.
+  std::optional<QualType>
+  tryMergeOverflowBehaviorTypes(QualType LHS, QualType RHS, bool OfBlockPointer,
+                                bool Unqualified, bool BlockReturnType,
+                                bool IsConditionalOperator);
+
   /// Return parsed documentation comment attached to a given declaration.
   /// Returns nullptr if no comment is attached. Does not look at any
   /// redeclarations of the declaration.
@@ -1843,6 +1856,13 @@ class ASTContext : public RefCountedBase<ASTContext> {
   QualType getBTFTagAttributedType(const BTFTypeTagAttr *BTFAttr,
                                    QualType Wrapped) const;
 
+  QualType getOverflowBehaviorType(const OverflowBehaviorAttr *Attr,
+                                   QualType Wrapped) const;
+
+  QualType
+  getOverflowBehaviorType(OverflowBehaviorType::OverflowBehaviorKind Kind,
+                          QualType Wrapped) const;
+
   QualType getHLSLAttributedResourceType(
       QualType Wrapped, QualType Contained,
       const HLSLAttributedResourceType::Attributes &Attrs);
diff --git a/clang/include/clang/AST/ASTNodeTraverser.h b/clang/include/clang/AST/ASTNodeTraverser.h
index 8ebabb2bde10d..e684dcd5c27e8 100644
--- a/clang/include/clang/AST/ASTNodeTraverser.h
+++ b/clang/include/clang/AST/ASTNodeTraverser.h
@@ -445,6 +445,9 @@ class ASTNodeTraverser
   void VisitBTFTagAttributedType(const BTFTagAttributedType *T) {
     Visit(T->getWrappedType());
   }
+  void VisitOverflowBehaviorType(const OverflowBehaviorType *T) {
+    Visit(T->getUnderlyingType());
+  }
   void VisitHLSLAttributedResourceType(const HLSLAttributedResourceType *T) {
     QualType Contained = T->getContainedType();
     if (!Contained.isNull())
diff --git a/clang/include/clang/AST/Expr.h b/clang/include/clang/AST/Expr.h
index 523c0326d47ef..4fbea7272d004 100644
--- a/clang/include/clang/AST/Expr.h
+++ b/clang/include/clang/AST/Expr.h
@@ -1477,6 +1477,14 @@ class DeclRefExpr final
     return DeclRefExprBits.IsImmediateEscalating;
   }
 
+  bool isOverflowBehaviorDiscarded() const {
+    return DeclRefExprBits.IsOverflwBehaviorDiscarded;
+  }
+
+  void setOverflowBehaviorDiscarded(bool Set) {
+    DeclRefExprBits.IsOverflwBehaviorDiscarded = Set;
+  }
+
   void setIsImmediateEscalating(bool Set) {
     DeclRefExprBits.IsImmediateEscalating = Set;
   }
diff --git a/clang/include/clang/AST/PropertiesBase.td b/clang/include/clang/AST/PropertiesBase.td
index 1215056ffde1b..26dd53760c3a2 100644
--- a/clang/include/clang/AST/PropertiesBase.td
+++ b/clang/include/clang/AST/PropertiesBase.td
@@ -81,6 +81,8 @@ def AutoTypeKeyword : EnumPropertyType;
 def Bool : PropertyType<"bool">;
 def BuiltinTypeKind : EnumPropertyType<"BuiltinType::Kind">;
 def BTFTypeTagAttr : PropertyType<"const BTFTypeTagAttr *">;
+def OverflowBehaviorKind
+    : EnumPropertyType<"OverflowBehaviorType::OverflowBehaviorKind">;
 def CallingConv : EnumPropertyType;
 def DeclarationName : PropertyType;
 def DeclarationNameKind : EnumPropertyType<"DeclarationName::NameKind">;
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h b/clang/include/clang/AST/RecursiveASTVisitor.h
index 5cb2f57edffe4..4a681a58b1cc9 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -1151,6 +1151,9 @@ DEF_TRAVERSE_TYPE(CountAttributedType, {
 DEF_TRAVERSE_TYPE(BTFTagAttributedType,
                   { TRY_TO(TraverseType(T->getWrappedType())); })
 
+DEF_TRAVERSE_TYPE(OverflowBehaviorType,
+                  { TRY_TO(TraverseType(T->getUnderlyingType())); })
+
 DEF_TRAVERSE_TYPE(HLSLAttributedResourceType,
                   { TRY_TO(TraverseType(T->getWrappedType())); })
 
@@ -1462,6 +1465,9 @@ DEF_TRAVERSE_TYPELOC(CountAttributedType,
 DEF_TRAVERSE_TYPELOC(BTFTagAttributedType,
                      { TRY_TO(TraverseTypeLoc(TL.getWrappedLoc())); })
 
+DEF_TRAVERSE_TYPELOC(OverflowBehaviorType,
+                     { TRY_TO(TraverseTypeLoc(TL.getWrappedLoc())); })
+
 DEF_TRAVERSE_TYPELOC(HLSLAttributedResourceType,
   ...
[truncated]

Copy link
Member

@jyknight jyknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple comments from a quick glance.

@JustinStitt JustinStitt force-pushed the overflow-behavior-types-pr branch from 645069d to a5d8ad5 Compare July 24, 2025 22:28
@JustinStitt
Copy link
Contributor Author

gentle ping.

@JustinStitt JustinStitt requested review from vitalybuka and kcc August 13, 2025 19:45
@JustinStitt JustinStitt force-pushed the overflow-behavior-types-pr branch from fad8d73 to 579a796 Compare August 18, 2025 18:30
@JustinStitt
Copy link
Contributor Author

ping.

I rebased due to some small conflicts.

question to reviewers: Should I squash these ~10 commits into one for ease of review?

@JustinStitt JustinStitt requested a review from mizvekov August 23, 2025 01:46
Copy link
Contributor

@mizvekov mizvekov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments, I will try a more through review later.

Copy link
Contributor

@mizvekov mizvekov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about inner qualifiers? The patch doesn't address the situation.

For a test like:

using ConstInt = const int;
using WrapConstInt1 = ConstInt __attribute__((overflow_behavior(wrap)));
using WrapConstInt2 = const int __attribute__((overflow_behavior(wrap)));

WrapConstInt1 and WrapConstInt2 should be the same type, as this is what makes most sense, I think.

This would be the same rules as qualifiers on array types, the inner and outer qualifiers mean the same thing and are added together for canonicalization.

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Aug 25, 2025

thanks @mizvekov for the initial review. I've tried to address all your concerns with commit 04af8bf. Let me know if anything there doesn't quite hit the mark - I'm happy to iterate.

As for your comment:

I am more concerned that these functions don't seem like a good fit for being part of ASTContext, maybe we can find a better home for them.

Do you think maybe that should be another PR that I can send later on? Should it come before this PR? Where do you think a better home is for that overflow pattern elision stuff?

And for your inner qualifiers question:

How about inner qualifiers? The patch doesn't address the situation.

How should we address these? FWIW, currently, the two types in your example do not compare equal:

  std::cout << std::boolalpha
            << std::is_same<WrapConstInt1, WrapConstInt2>::value << "\n"; // false

I am not sure what you mean by "inner qualifiers" from your example. My understanding of inner qualifiers involves pointer types like const int *p vs int * const p.

@JustinStitt JustinStitt requested a review from mizvekov August 25, 2025 23:16
@mizvekov
Copy link
Contributor

Do you think maybe that should be another PR that I can send later on? Should it come before this PR? Where do you think a better home is for that overflow pattern elision stuff?

I think this is a simple enough change, it's just about moving where the function is implemented.
We can do it in this PR. This is not a blocking concern from me, it just seems to me like this function depends on just
too little from ASTContext in order to be part of it, and it doesn't quite fit the overall theme.

I think Type.h would be a better fit, but I'd wait a little bit for other opinions before changing anything.

How should we address these? FWIW, currently, the two types in your example do not compare equal:

  std::cout << std::boolalpha
            << std::is_same<WrapConstInt1, WrapConstInt2>::value << "\n"; // false

I am not sure what you mean by "inner qualifiers" from your example. My understanding of inner qualifiers involves pointer types like const int *p vs int * const p.

So there are two positions qualifiers can appear in this type.

  1. Qualifiers over the OverflowBheaviourType itself. Lets call those the outer qualifiers.
  2. Qualifiers over the underlying type of OverflowBheaviourType. Lets call those the inner qualifiers.

It doesn't seem plausible to me that these qualifiers have different meanings, that is to say, that the two types in my example should be different types, instead of two spellings of the same type.

One solution here would be to adopt the same rules as ArrayTypes for this situation.
An array of const int is the same as a const array of ints, which is also the same as a const array of const ints.

You can implement this by changing your canonicalization for OverflowBheaviourType, making it remove the qualifiers from the underlying type, and add those as qualifiers over the OverflowBeaviourType.

@JustinStitt
Copy link
Contributor Author

It doesn't seem plausible to me that these qualifiers have different meanings, that is to say, that the two types in my example should be different types, instead of two spellings of the same type.

I'm getting conflicting ideas from this sentence. Do you mean to say that the two types from your example should be the same type?

@JustinStitt
Copy link
Contributor Author

FYI, I'm meeting with @kees off-thread today to analyze kernel use cases and digest @ojhunt's review comments.

@ojhunt
Copy link
Contributor

ojhunt commented Sep 24, 2025

FYI, I'm meeting with @kees off-thread today to analyze kernel use cases and digest @ojhunt's review comments.

Sorry for the wall of text, and again sorry for not seeing the RFC :(

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Sep 24, 2025

Sorry for the wall of text, and again sorry for not seeing the RFC :(

All good, your review is vital. I really have no personal attachments to any particular design decisions made -- I want to make sure this feature is useful to as many developers as possible.

Quick question before I go meet with Kees: How do you propose we handle overflow semantics with less-than-int stuff.

The core reason all of this narrowing and promotion magic exists is to solve the problem of:

u8 __ob_wrap a = 255;

if (a+1 == 0){...} // 1 is `int`, therefore whole expression promoted to `int`.

// Whoops! We no longer wrap-around at the 8-bit boundary because we are in `int` type boundaries now.

Not truncating things back down to the wrap type renders the __ob_wrap essentially useless for less-than-int types. We need to persist bitwidth information somehow. The current approach just narrows everything to the OBT type. Another approach mentioned in the RFC is to store the widths in some data structure. But I can see some issues with that approach too.

So, @ojhunt how do you ideally want all these narrowing semantics to work?

@ojhunt
Copy link
Contributor

ojhunt commented Sep 25, 2025

Sorry for the wall of text, and again sorry for not seeing the RFC :(

All good, your review is vital. I really have no personal attachments to any particular design decisions made -- I want to make sure this feature is useful to as many developers as possible.

Quick question before I go meet with Kees: How do you propose we handle overflow semantics with less-than-int stuff.

The core reason all of this narrowing and promotion magic exists is to solve the problem of:

u8 __ob_wrap a = 255;

if (a+1 == 0){...} // 1 is `int`, therefore whole expression promoted to `int`.

// Whoops! We no longer wrap-around at the 8-bit boundary because we are in `int` type boundaries now.

You would need to give me some indication that this is a behavior developers want or expect.

I think it's useful to think about how this attribute will be used in practice, including the linux kernel:

#if __has_attribute(...) // or has keyword or whatever
#define __wrap_attr __obt_wrap
#else
#define __wrap_attr
#endif

Given that usage - which is how it would have to be set up - how would your example of

uint8_t __wrap_attr a = 255;
if (a + 1 == 0) { ... }

To work in any real use case? It will not work on any compiler that does not have this attribute.

But this code is also fundamentally not realistic - a real version of this code would look like:

uint8_t __wrap_attr a = 255;
if (a + 1 > 0xff) { ... }

Except with the __wrap_attr this will no longer ever be true.

In fact any type bounds check like this does not work:

if (some_int + some_intN > __INTN_MAX__)

The only case that the truncation of large values does is your above if (a + 1 == 0) which is simply not a realistic scenario.

Not truncating things back down to the wrap type renders the __ob_wrap essentially useless for less-than-int types. We need to persist bitwidth information somehow. The current approach just narrows everything to the OBT type. Another approach mentioned in the RFC is to store the widths in some data structure. But I can see some issues with that approach too.

Why do you think you need to do this? The problem with overflow is not "the value is larger than I expected" it is "overflow leads to UB allowing compiler introduced vulnerabilities", and "ftrapv and fwrapv are global, but we want different behavior in different contexts".

This change to narrowing does not have anything to do with that.

So, @ojhunt how do you ideally want all these narrowing semantics to work?

The exact same way promotion works today: there should not be any narrowing. There is no reason to change this behavior to narrow: it makes the feature confusing, introduces security vulnerabilities, makes adoption unusable, and literally discards data in ways that produce warnings in many default configuration. Implicit narrowing is widely regarded as a mistake nowadays, and this not only adds a new version of implicit narrowing, it removes the warnings about that narrowing.

Returning to your example:

if (a + 1 == 0) { .. }

this is not something I believe anyone would expect, and more to the point is not something anyone could actually use. Removing narrowing does not make the wrapping behavior meaningless:

int f(bool b) {
  int8_t __obt_trap a = 1;
  int32_t i = __INT_MAX__ ;
  if (b)
    i = i + a; // int32_t + uint8_t __obt_trap => int32_t + int32_t __obt_trap => trap due to the overflow
  return i;
}

But more to the point, consider this:

int64_t a = ...;
int32_t __obt_wrap /* or __obt_trap */ b = ...;
int64_t result = a + b;

Would any developer would expect a truncated 32bit result? or an overflow trap? That doesn't match -ftrapv or -fwrapv, or any hypothetical future "erroneous behavior" specification for overflow.

We can also consider an unsigned version of the above:

uint64_t a = ...;
uint32_t __obt_wrap /* or __obt_trap */ b = ...;
uint64_t result = a + b;

Overflow of a + b is well defined, so for unsigned __obt_wrap would reasonably be expected to be a no op (imagine an template that uses __obt_wrap in an attempt to have defined overflow for signed type arguments, knowing that unsigned already has wrapping behavior), and now that is (1) not true, and (2) truncates a previously full sized type.

Basically, rather that asking "how would if (a + 1 == 0) work?" when that''s simply not semantics that I believe any developer would expect. Do you believe that there are developers that would expect or want to not be pass for any combination of (non-obt qualified) integer types:

template <class A, class B> void f(A a, B b) {
  static_assert(same_type_v<decltype((A)0 + (B __wrap)0),  decltype((A)0 + (B)0) __obt_wrap>);
  static_assert(same_type_v<decltype((A __wrap)0 + (B)0),  decltype((A)0 + (A)0) __obt_wrap>);
  static_assert(same_type_v<decltype((A)0 + (B __obt_trap)0),  decltype((A)0 + (B)0) __obt_trap>);
  static_assert(same_type_v<decltype((A __obt_trap)0 + (B)0),  decltype((A)0 + (A)0) __obt_trap>);
}

Because these qualifiers introduce implicit exposure that is not directly exposed in local source, e.g that types below could be auto, the result of direct field access (e.g. no local declared), function returns (again no local), means you can easily get code that does not appear to involve these qualifiers, but that is semantically equivalent to:

uint64_t u64 = 1ull<<32;
uint32_t __wrap u32 = 1;
u64 = u64 + u32;

Which from what I can tell results in u64 being equal to 1, despite that all the existing language semantics say that the arithmetic should be 64bit.

Let's imagine a warning about this then all of the existing correct code now needs to have casts everywhere, and those casts can easily drop the qualifier, ie. the above would need to be "corrected" with

uint64_t u64 = 1ull<<32;
uint32_t __obt_wrap u32 = 1;
u64 = u64 + (some_type)u32;

But what should some_type be? if some_type is uint32_t you drop wrapping rules where you may not actually need to (e.g auto, etc), or should it be u64 __obt_wrap or u64 __obt_trap? if this is generic code that may not have a single answer - you'd likely end up needing to create a pile of templates to get the correct type (according to normal integer operations), and I don't know if it's even addressable in C, but it doesn't have templates so maybe less relevant.

@rnk
Copy link
Collaborator

rnk commented Sep 25, 2025

I apologize I haven't carefully read @ojhunt 's self-admitted wall of text, but it seems to me like a possible next step would be to set up a call to come up with some acceptable compromise.

My high-level input is, I wonder if Oliver's concerns about __wrap can be fixed by using more precise math-y naming by invoking the word "modular", or "mod2". Yeah, I know, it's bikeshedding, but names are important, they are the essentially unskippable documentation. As a reader, this seems like the meaning of the examples you are passing back and forth would be clearer:

uint8_t __mod2 x = ...;
if (x + 1 == 0) { ... } // Is there a special case to narrow integer literals?
...
uint64_t u64 = 1ull<<32;
uint32_t __mod2 u32 = 1;
u64 = u64 + u32; // Do we widen u32, and then do a `+mod2` operation?
// Does the assignment from `uint64_t __mod2` allow implicit conversion?

If you think about the canonical use case for intentional unsigned integer overflow, it's probably cryptographic algorithms, where the algorithms are usually defined in terms of modular arithmetic. If we call it that, something math-y programmers will be less likely to reach for the feature when they are simply trying to implement an overflow check.


As for __nowrap, my imperfect understanding is that it basically constrains the implementation to either behave as -fwrapv or -ftrapv. It would be incorrect to apply LLVM nsw / nuw qualifiers to the resulting math instructions.

@ojhunt
Copy link
Contributor

ojhunt commented Sep 25, 2025

I apologize I haven't carefully read @ojhunt 's self-admitted wall of text, but it seems to me like a possible next step would be to set up a call to come up with some acceptable compromise.

My high-level input is, I wonder if Oliver's concerns about __wrap can be fixed by using more precise math-y naming by invoking the word "modular", or "mod2". Yeah, I know, it's bikeshedding, but names are important, they are the essentially unskippable documentation. As a reader, this seems like the meaning of the examples you are passing back and forth would be clearer:

uint8_t __mod2 x = ...;
if (x + 1 == 0) { ... } // Is there a special case to narrow integer literals?

As I said previously, this behavior would be unusable in any codebase that needs to share code with compilers that don't have this behavior, and special casing literals would be even worse.

Consider real world existing code:

// the existing *correct* version of the above:
if (x + 1 == 256) { .. } // now broken
// or more realistically
if (x + 1 > 255) { .. } // now broken

For special casing literals:

uint8_t __wrap x = 255;
if (x + 1 == 0) { ... } // "works"
constexpr uint8_t some_const = 1;
if (x + some_const == 0) { ... } // does not work

Of course neither of these cases are compatible with code that needs to support non-__obt_wrap providing compilers.

...
uint64_t u64 = 1ull<<32;
uint32_t __mod2 u32 = 1;
u64 = u64 + u32; // Do we widen u32, and then do a +mod2 operation?

my understanding is that this is:

u64 = (u64 % __UINT_MAX__) + u32

I simply cannot believe any developer would ever expect this behavior.

// Does the assignment from uint64_t __mod2 allow implicit conversion?

There is no uint64_t __mod2 in the above expression, the expression truncates the uint64_t to uint32_t and then widens to uint64_t .

There's also some basic consistency questions:

uint32_t __wrap u32 = 32;
uint64_t u64 = 1ull<<u32;

The same logic that says 1ull + u32 should be interpreted as uint32_t(1ull) + u32 applies just as well to <<.

If you think about the canonical use case for intentional unsigned integer overflow, it's probably cryptographic algorithms, where the algorithms are usually defined in terms of modular arithmetic. If we call it that, something math-y programmers will be less likely to reach for the feature when they are simply trying to implement an overflow check.

The question is: What is the use case for this feature? It sounds like the intent is to remove UB from overflow, and the presented use case is the linux kernel, but I cannot see how the kernel could possibly use this feature as currently defined.

As for __nowrap, my imperfect understanding is that it basically constrains the implementation to either behave as -fwrapv or -ftrapv. It would be incorrect to apply LLVM nsw / nuw qualifiers to the resulting math instructions.

It does not. The narrowing behavior means it is not remotely equivalent. -fwrapv/-ftrapv do not change correct program behavior, the only thing they do is make overflow well defined. Specifically -fwrapv does not change any behavior of any existing code, with the exception of removing compiler injected security vulnerabilities from UB abuse.

If we insist on using this narrowing semantic, it should not be pretending that an unqualified type is "compatible" with the other sized type. There is absolutely no reason to produce an error message for

uint32_t __wrap u32 = ..;
uint64_t __wrap u64 = ..;
return u32 + u64;

but not produce an error for

uint32_t __wrap u32 = ..;
uint64_t u64 = ..;
return u32 + u64;

The correct way to approach to thinking about these qualifiers is that they are not a boolean: {obt_wrap, obt_trap, /*implicit*/ obt_undefined} (honestly making the internal implementation enum have obt_undefined will simplify logic that currently has to do if (has_obt && obt_mode == ...), which makes it clear that (uint32_t __wrap) + (uint64_t) is a mismatching type.

This has to be an error because as previously stated the alternative is literally introducing security vulnerabilities, and introducing them non locally. Consider:

// SomeHeaderSomeWhere.h
struct S {
   uint32_t count;
   ...
};
// SomeUnrelatedFile.cpp
...
SomeType *alloc(struct S* s, ) {
  return (SomeType *)malloc(sizeof(SomeThing) * s->count);
}

Later someone removes the UB:

// SomeHeaderSomeWhere.h
 struct S {
-   uint32_t count;
+   uint32_t __wrap count;
    ...
 };

The malloc call can now under allocate.

Code that is doing the correct bounds checks also now fails:

uint64_t a = ...; // Generally comes from something like sizeof, so it would be size_t
uint32_t __wrap b = ...; // May come from a field, intermediate computation, etc
if ((a + b) < b) { .. } // unsigned overflow is well defined, so this test is correct, but now it is not

Again, I cannot think of any case where anyone would expect a + b to ever produce a smaller type than the inputs. It should be just as much an error if the underlying types don't match when there is one argument is qualified and the other is not, as it would be if both arguments have the same type.

@JustinStitt supporting the linux Kernel was presented explicitly as part of the justification, so I would really like you to check with the people in the linux kernel who are wanting this feature to see if they believe that this feature is usable, and in those discussions I would want their feedback on the examples of what I consider unsafe or incompatible behavioural changes directly, and the behavioral differences if they use the obvious adoption path of

#if some_check_for_the_feature
#define __wrap_qualifier_name __obt_wrap
#define __trap_qualifier_name __obt_trap
#else
#define __wrap_qualifier_name
#define __trap_qualifier_name
#endif
struct S {
   uint32_t __wrap_qualifier_name field;
};
...
alloc(sizeof(Thing) * someS.field); // under-allocates if __obt_wrap is available, works correctly if it is not
if (some_uint64 + someS.field < __UINT64_MAX__) ... // always succeeds if __obt_wrap is available

I'm not sure what the exact ordering of narrowing, but that only matters for __obt_trap, __obt_wrap just loses data:

uint32_t __obt_trap a = ...;
uint64_t b = ...;
uint64_t c = a * b;

if the order of operations is a * (uint32_t)b the overflow might never occur, so the trap goes away, whereas (uint32_t __obt_trap)((uint64_t)a * b) can in principle trap if the uint64_t result is out of the uint32_t range (and honestly I believe __obt_trap should trap on narrowing operations that truncate, which would mitigate the security vulnerabilities but at the cost of causing traps on what was overflow free code).

From a usability point of view, the narrowing behavior is the reason you are diagnosing an error on (uint64_t __wrap value) + (uint32_t __wrap value), if that narrowing behavior did not exists the result would be of type uint64_t __wrap which is what would I believe would be the expected outcome of every developer. Because it now truncates, it will need to produce the same diagnostics provided for mismatched qualifiers not just silently truncate. Because of this previously correct code will now need to be littered with casts, and if it does not produce at the very least a warning it will cause changes in one piece of code to introduce security vulnerabilities in others.

If the narrowing behavior is removed the result is:

  • It can be adopted when a code base needs to support building with compilers that don't support the feature
  • Code that was correct without this feature remains correct when obt qualified types enter the logic.
  • The non-ergonomic error/warning for expressions of the form type1 __obt_same_qualifier a = ..; type2 __obt_same_qualifier b = ..; a + b produces (widest_type __obt_same_qualifier ) and continues to compile and be correct without the addition of warnings/errors, that can only be fixed by the introduction of many casts everywhere.
  • Existing macros or template functions do not need to jump through many hoops to maintain correct qualifiers (e.g. decltype(a + b) (or C/macro equivalent) no longer works as expected, you need to remember in all code to strip the qualifiers, but only if the qualifiers match, and restore the qualifier, or require every use to be updated to include correct casts, that may also have the same problem.
  • It remains safe to use decltype, auto, unconstrained template parameters, and all the C equivalents unsafe, because it removes the hazard induced by the viral nature of the truncating behavior
  • It removes the hazard of incorrect "corrective casts":
    int_type_32_t __wrap a = ...;
    int_type_64_t b = ...;
    return a + b; // produces a warning, what is the fix?
    • Changing to a + (int32_t)b -> you lose defined overflow behavior: no defined wrapping (for signed types), no overflow trapping (for signed or unsigned types)
    • Changing it to a + (decltype(a))b -> incorrect if a is the smaller type
    • Changing it to (decltype(b))a + b -> incorrect if b is the smaller type
    • If unqualified + qualifier silently truncates, but dropping the qualifier requires a cast: (int_type_int64_t)(a + b) -> completely silences diagnostics, but the truncation has already happened
  • It doesn't break existing code, in ways that are not necessarily directly security errors (though of course may result in incorrect state that introduces vulnerabilities):
    buffer[bigint_value + smaller_wrapping_value]++; // not updating things correctly
    if (bigint_value + smaller_wrapping_value > y) ...; // truncation means this comparison fails incorrectly

Basically, the only real justification you have given for this behavior that you have given is the if ((wrapping_char)0xff == 0) case, which is not a use case I have ever heard anyone asking for, existing code that does need that kind of test already does (smalltype)(expression) operator value. In exchange for that dubious use case that the feature makes adoption directly hazardous, makes adoption when multiple compilers need to be supported impossible, makes the ergonomics much worse due to the need for otherwise unnecessary widening casts, and makes the feature vastly more complicated to reason about due to the completely different semantics from all other arithmetic.

The only thing I have ever heard developers ask for is:

  • Overflow not being UB, because the only time that matters is when the compiler proves it happens, but does so silently and introduces security bugs.
  • Being able to select wrapping vs trapping behavior with finer grain than the global flags as otherwise -fwrapv is needed to "guarantee" no spurious crashes due to intentional wrapping and realistically wrapping is the actual behavior the code currently assumes, and resolving those correctly with ftrapv is extremely verbose and annoying, while they've already got code that tries to correctly handle unwanted overflow manually, but they don't have a safety new for cases where they miss the required checks.

Again, I'd like to you to ask kernel folk if this is a behavior that they actually want (if they do, it would be extremely surprising to me), but certainly in every codebase I'm aware of, the feature would not only not be usable, it would need to be explicitly banned (as in: static assertions that is not enabled, linters to prevent it being introduced, etc).

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Sep 25, 2025

@JustinStitt supporting the linux Kernel was presented explicitly as part of the justification, so I would really like you to check with the people in the linux kernel who are wanting this feature to see if they believe that this feature is usable, and in those discussions I would want their feedback on the examples of what I consider unsafe or incompatible behavioural changes directly, and the behavioral differences if they use the obvious adoption path of...

Note: I haven't read your entire response yet.

I met with @kees (kernel hardening fellow) off-thread and I believe he is crafting a comment regarding kernel use to post here. We read your review comments (except this most recent one) and discussed the path forward. I don't want to preempt his reply, though.

At any rate, we should get some clang folks together and have some more live discussions.

...Now, back to reading another wall of text 😉

@ojhunt
Copy link
Contributor

ojhunt commented Sep 25, 2025

...Now, back to reading another wall of text 😉

It's a skill.... whether it's a good skill is an entirely different question :D

@kees
Copy link
Contributor

kees commented Sep 25, 2025

Hello! Time for my wall of text. ;) But no joke, @ojhunt thank you for your walls of text. I found your examples and thought processes really helpful in trying to find a way to conceptually navigate this space. I'm also glad to see some themes in your more recent reply mirror some of the discussion @JustinStitt and I had today. But let me dive in:

Firstly, I see much of the discussion focuses on __ob_wrap, but what I see Linux using from this work is almost exclusively __ob_trap. So, from that perspective, I seen no reason to have alternative narrowing for the __ob_wrap usage, except that having differing behaviors between wrap and trap would risk very high confusion about their use. Regardless, I think my point is, my evaluative process for this feature mostly centers around how __ob_trap can be used to avoid unexpected arithmetic outcomes, and that __ob_wrap is a bonus to effectively drop UB for signed type wrapping at the source level (i.e. not needing -fno-strict-overflow/-fwrapv).

I took a look through a random handful of recent Linux integer overflow flaws that got fixed, and 9 out of 10 had no dependency on the alternative narrowing, so using standard narrowing still shows a strong benefit to Linux. So I'm not opposed to using standard narrowing, but I do want to explore the goals around why we wanted it originally.

First, to give an example of where alternative narrowing is helpful, which I'll discuss below:

u16 calculate(...) {
  u16 __ob_trap result = 0;
  int something;
  // ...
  return result - something;
}

If we wanted anything involving a __ob_trap to be able to catch overflows, the above only works with alternative narrowing. Standard promotion would take result - something and make it an int (actually an int __ob_trap), which means we could go negative (with no overflow trap). And the return type being non-obt would silently truncate the result. So the intention of "make sure calculations involving result will trap" gets lost. With alternative narrowing, result - something treats the expression as u16 __ob_trap and the underflow gets trapped.

Observations:

  • if something were int __ob_trap, we would also fail to trap since matched "kinds" (both OBTs) don't narrow, and so result gets promoted to int __ob_trap. This certainly feels inconsistent, but this was done to try to make adoption easier for a given code base. (More on this below.)
  • if calculate returned u16 __ob_trap, we'd have no problem: truncation would catch it.

So, the goal of narrowing the mismatched Kind, I realize now, was mainly around C's ambiguity of arithmetic operation's width given commutative operation. If operations were directional we might infer bit width from the "value being operated on": "subtract something from result (which has u16 width)", which is what we were trying to do. The functional arithmetic builtins (e.g. __builtin_check_mul_overflow) solve this by making the target width explicit. But no C programmer I know is excited about switching to functional arithmetic. ;) I imagine a wild way to declare this in the language by casting the operator, like result (u16)- something but I cannot imagine a way to make that acceptable to C programmers. :)

Looking at Rust's arithmetic, there is just simply no mismatched bit widths allowed. Things need to be explicitly cast, so there is no ambiguity about bit width. Going all the way to this requirement feels like the furthest swing away from usability in C, especially with the goal of slowly adding __ob_trap types/annotations to an existing codebase.

So, I think the spectrum of solution is: "require matching kinds and widths" to "use standard narrowing". @JustinStitt and I considered two possible compromises:

  • use standard narrowing/promotion but preserve bit width overflows along the way, and surface it at the end somehow
  • use sub-int promotion only with matching Kinds, e.g. u8 __ob_trap + u16 __ob_trap results in u16 __ob_trap not int __ob_trap

The case that I find most troublesome (and has the largest security impact) is that of wrapping positive during int promotion, which I think is handled by making sure that promotions always promote toward __ob_trap when present. For example:

int a, b, c;
a = INT_MAX;
b = INT_MAX - 99;
c = a * b; // wraps to 100

If a or b is __ob_trap, the calculation needs to use __ob_trap to see the overflow. If only c is __ob_trap, it'll miss the overflow, so it'd be nice if the __ob_trap got applied too.

I'm not sure my wall of text ended up as organized as I want it, but I think the problem we saw as unsolved was dealing with commutativity for catching overflows meaningfully: we either need directionality or homogeneity. Directionality is hard to articulate in the language, and homogeneity requires assumptions/ambiguity.

@rjmccall
Copy link
Contributor

I'm sorry for not having previously contributed to this discussion, but I was wondering if we had considered using a pragma which could be attached to a lexical scope (like #pragma STDC FENV_ROUND) rather than changing the type system. I think that would both be significantly easier for users to wrap their heads around and much more standardizable. Anyway, I wrote it up in the RFC.

@ojeda
Copy link

ojeda commented Sep 26, 2025

Looking at Rust's arithmetic, there is just simply no mismatched bit widths allowed. Things need to be explicitly cast, so there is no ambiguity about bit width. Going all the way to this requirement feels like the furthest swing away from usability in C, especially with the goal of slowly adding __ob_trap types/annotations to an existing codebase.

To be honest, not having to worry all the time about that in Rust is actually quite nice, and while skimming this discussion I felt it would be the simplest approach here too. But, yeah, some kernel developers may not be thrilled about it being an error -- it is very not-C-like. It would also require building the kernel often with a newer Clang, to catch all missing casts. It also feels a bit orthogonal to the overflow behavior (but since they do not behave the same anyway, it is a fair opportunity to introduce integers with less surprises...).

@ojhunt
Copy link
Contributor

ojhunt commented Sep 26, 2025

I'm really sorry, I tried to trim this down. So here's a couple of the new problems I've added to this one:

  • Breaking correct bounds checks:
    uint8_t __obt_trap_wrapper u;
    if (__UINT32_T_MAX__ - u < some_other_value)
      return;
    if (u + some_other_value > some_int) {
      ...
    }
    It does not matter when/where narrowing occurs, it goes wrong in cases it was correct: the bounds check itself can trap and/or it incorrectly evaluates to false due to the narrowed type after the - evaluation.
  • If the attribute is used in APIs or header types in conjunction with any kind of deduced types (decltype, templates, macros as used in many C code bases, etc) the narrowing behavior results in ABI changes. In C this is purely an ABI layout mismatch, in C++ even if the obt qualifiers are stripped the change underlying type changes the symbol name (on the plus side that might show up as a link failure).
  • trapping behavior triggers even if the final result of an expression is in bounds.

** actual wall **

Firstly, I see much of the discussion focuses on __ob_wrap, but what I see Linux using from this work is almost exclusively __ob_trap...

The focus on __ob_wrap is because it is silently problematic, if trapping is the only behavior that is wanted, then __objt_wrap should not be included at all.

But narrowing with __ob_trap is also problematic, take

uint32_t __obt_trap u32 = ...;
uint64_t u64 = ...;
uint64_t result = u32 + u64;

Lets assume (uint64_t)u32 + u64 is greater than 2^32 - 1, without __obt_*, any result with obt is wrong. Consider how u32 + u64 is expected to be evaluated:

  • u32 + (uint32_t)u64 -> truncated result, no trap
  • u32 + clamp_u32(u64) -> hypothetical clamp_* traps if the input does not fit in the target. Correct code traps.
  • clamp_u32((uint64_t)u32 + u64) -> traps on correct code

And of course the same virality issues applies, where a single such attributed type will cause narrowing throughout an entire expression evaluation, which is either truncated incorrectly, or traps incorrectly.

I took a look through a random handful of recent Linux integer overflow flaws that got fixed, and 9 out of 10 had no dependency on the alternative narrowing, so using standard narrowing still shows a strong benefit to Linux. So I'm not opposed to using standard narrowing, but I do want to explore the goals around why we wanted it originally.

Right - my entire problem is the narrowing, as described previously I believe it renders it unadoptable in the kernel due to the completely different semantics, and even ignoring the compatibility problems, I believe that the semantics will be completely unexpected.

The question I'm wanting kernel folk to answer is whether they would be ok with something like

uint8_t __obt_trap_wrapper u;
if (u + some_other_value > 0xff) {

}

Either always returning false (wrapping to uint8_t), or trapping in the overflow (?) check due to early narrowing, or returning false incorrectly but only if obt is enabled, and if obt is not enabled the only overflow exists on the promoted, but that can be prevented with:

uint8_t __obt_trap_wrapper u;
if (__UINT32_T_MAX__ - u < some_other_value)
  return;
if (u + some_other_value > 0xff) {

}

But now even that overflow test is incorrect, no matter how or when the truncation happens: the overflow test now traps in some cases and produces incorrect false results in others.

First, to give an example of where alternative narrowing is helpful, which I'll discuss below:

u16 calculate(...) {
  u16 __ob_trap result = 0;
  int something;
  // ...
  return result - something;
}

If we wanted anything involving a __ob_trap to be able to catch overflows, the above only works with alternative narrowing. Standard promotion would take result - something and make it an int (actually an int __ob_trap), which means we could go negative (with no overflow trap). And the return type being non-obt would silently truncate the result. So the intention of "make sure calculations involving result will trap" gets lost. With alternative narrowing, result - something treats the expression as u16 __ob_trap and the underflow gets trapped.

I don't believe this statement to be true. The model I believe is correct is that the above is equivalent to:

u16 calculate(...) {
  u16 __ob_trap result = 0;
  int something;
  i32 __ob_trap widened_result = result; // I _think_ this is the correct widening rule :D
  i32 __ob_trap temp1 = widened_result - (i32 __ob_trap)something;
  u16 __ob_trap temp2 = (u16 __ob_trap)temp1; // traps if this overflows
  return temp2;
}

The issue is not trapping/wrapping on narrowing, it's that the narrowing is happening at a point where all existing mismatched width integer arithmetic either widens, or is an error.

Observations:

  • if something were int __ob_trap, we would also fail to trap since matched "kinds" (both OBTs) don't narrow, and so result gets promoted to int __ob_trap. This certainly feels inconsistent, but this was done to try to make adoption easier for a given code base. (More on this below.)
  • if calculate returned u16 __ob_trap, we'd have no problem: truncation would catch it.

I think this is the misunderstanding - the overflow mode would propagate: if there's an operation where only one input has an explicit overflow behavior, that is propagated, and I would really want removal of those tags to require an explicit cast, e.g.

u16 calculate(...) {
  u16 __ob_trap result = 0;
  int something;
  return (u16)(result - something);
}

To address unintentional truncation, my belief is that this would be semantically equivalent to (u16)(u16 __obt_mode)(result - something), eg obt_wrap would have defined wrapping behavior to the target size, obt_trap would trap if result - something was not in the range of the destination type.

One thing I think would be interesting would be whether such explicit casts would be required to explicitly drop the obt mode in such casts, something like __obt_none (which in practice would be equivalent to __obt_wrap but distinguishing "I am intentionally removing the qualifier" vs "I am changing the mode" might be good?

So, the goal of narrowing the mismatched Kind, I realize now, was mainly around C's ambiguity of arithmetic operation's width given commutative operation. If operations were directional we might infer bit width from the "value being operated on": "subtract something from result (which has u16 width)", which is what we were trying to do. The functional arithmetic builtins (e.g. __builtin_check_mul_overflow) solve this by making the target width explicit. But no C programmer I know is excited about switching to functional arithmetic. ;) I imagine a wild way to declare this in the language by casting the operator, like result (u16)- something but I cannot imagine a way to make that acceptable to C programmers. :)

The thing is that I believe developers have no difficulty understanding the widening behavior, but this narrowing behaviour is extremely unintuitive: why would adding a narrower value to a wider one ever be expected to produce a narrowed result?

Thing I just realised

All this discussion has been in the context of differing width, what happens with same width but mismatched signedness? e.g

u32 __obt_X a = ...;
i32 b = ...;
type1 r1 = a + b;
u32 c = ...;
i32 __obt_X d = ...;
type2 r2 = c + d;

I think my understanding of the expected semantics would mean type1 is not the same as type2, which adds even more confusion to the semantics of signed vs unsigned promotion. Honestly I would say that given the clearly confusing signed vs unsigned promotion - sidemotion? - rules, any mixed sign expressions where any obt mode is present should be an error.

I think we would also want to warn if we see at least a warning for things like:

(target_type)differently_signed_value + target_type_value

Because the unqualified target_type cast could unintentionally truncate/overflow (dropping negative, wrapping to negative, etc).

Looking at Rust's arithmetic, there is just simply no mismatched bit widths allowed.

Honestly I think that would not be terrible, it would certainly be preferable to unexpected narrowing, but you get into ergonomic issues with literals some_nont_int_obt_value + unsuffixed_literal - ergonomics mean we don't want to require manual annotation (is there actually a suffix for int or char literals? I just realized I don't actually know :D). The problem with literals is that if we want to maintain the narrowing behavior you get overflows where there should not be, you get differences in behavior when obt modes are available vs not, and clean up refactorings also change behavior, e.g

u16 __obt_mode x = ...;
x + 1 // a literal
x + (1 << 10) // not a literal but generally treated as such
const int refactored = 1 << 10;
x + refactored // no longer a literal but what should be a semantic no-op

Things need to be explicitly cast, so there is no ambiguity about bit width. Going all the way to this requirement feels like the furthest swing away from usability in C, especially with the goal of slowly adding __ob_trap types/annotations to an existing codebase.

Right, I think widening should be permitted, though the widening would propagate obt qualifiers, dropping obt qualifiers should require a cast. It's probably worth emitting a warning in compound expressions that include arithmetic on unqualified values leading into arithmetic with qualified ones.

So, I think the spectrum of solution is: "require matching kinds and widths" to "use standard narrowing". @JustinStitt and I considered two possible compromises:

  • use standard narrowing/promotion but preserve bit width overflows along the way, and surface it at the end somehow

I still don't understand what hazard you are trying to prevent here, narrowing only applies if the obt type is the narrower/different type, so:

  • If you widen the obt qualified type, the widened type still has the obt qualifier
  • If you perform a subsequent operation involving a narrower type it's promoted and you retain the selected obt semantics
  • If you then try to store the widened type into a narrower type, the obt qualifier applies to the narrowing operation

These semantics also mean you get sensible and consistent behaviour for obt qualified bitfields as well (i'm not sure if the current proposal permits application to bitfields, but these semantics result in this being reasonable):

struct S {
  uint32_t __obt_trap field: 20;
}

// with narrowing consistency would imply that this actually should be
// uint20_t __obt_trap. With standard widening that weirdness goes away
// and the result is uint32_t __obt_trap
auto u32 = 100 + someS.field;
S.field = u32; // this then traps if the value is >= 1<<20
  • use sub-int promotion only with matching Kinds, e.g. u8 __ob_trap + u16 __ob_trap results in u16 __ob_trap not int __ob_trap

See i don't understand why this would be expected behavior - I (and i would expect most devs) would expect the result to be decltype((u8)0 + (u16)0) __ob_trap. Subsequent narrowing would trap if the narrowing would be out of bounds.

The case that I find most troublesome (and has the largest security impact) is that of wrapping positive during int promotion, which I think is handled by making sure that promotions always promote toward __ob_trap when present. For example:

As stated above that safety depends on the point at which the narrowing truncation happens. If such operations are performed by first narrowing the wider type, if that loses information without trapping/wrapping, you truncate from the correct behavior, resulting in incorrect result and that result may be invalid, and you trap on the narrowing operation you can end up with operations that would have produced an in bounds result would trap.

I've given examples of where the cast point may impact out of bounds result, but the in bounds result case is also worth considering:

uint64_t u64 = 1ull<<32;
uint32_t __obt_trap u32 = 1;
// converting u64 to u32 at the start traps incorrectly
uint32_t __obt_trap result = u64 - u32;
int a, b, c;
a = INT_MAX;
b = INT_MAX - 99;
c = a * b; // wraps to 100

If a or b is __ob_trap, the calculation needs to use __ob_trap to see the overflow. If only c is __ob_trap, it'll miss the overflow, so it'd be nice if the __ob_trap got applied too.

Yup, that's why I wondered if we want to issue warnings if part of an integer expression is qualified and part is not but performs operations that may overflow

I'm not sure my wall of text ended up as organized as I want it, but I think the problem we saw as unsolved was dealing with commutativity for catching overflows meaningfully: we either need directionality or homogeneity. Directionality is hard to articulate in the language, and homogeneity requires assumptions/ambiguity.

I do not believe any approach that involves implicit narrowing is safe - it makes conditional adoption unsound due to the extremely different semantics across compilers, it changes deduced types, and it makes code that would reasonably be expected to produce a widened result (both due to that being the semantics everywhere else, and because it does not make sense to narrow one of the inputs). I think that the different types that this implicit narrowing produces can possibly result in abi/odr problems, that introduce security bugs:

struct APIStruct {
  u32 __conditional_obt_mode_wrapper a;
  template <integral intype> auto f(std::span<intype> in) -> std::vector<decltype(a + in[0])> {
    std::vector<decltype(a + *out)> v;
    for (auto x : in)
      v.push_back(x + a);
    return v;
  }
}

These now have different sizes, and if the output of f is passed to an implementation with a different obt mode, then use either get a potential buffer overflow, or incomplete initialization. This problem occurs whether obt_wrap or obt_trap is used.

This is technically an ODR violation, but if your code is compiled with different compilers, or different modules, this seemingly innocuous "remove overflow UB" flag results in an ABI mismatch. The fix for this is to not ever use the qualifier in any non-local context - e.g. local variables only. The ODR violation is only due to the narrowing behaviour, but the viral nature of that narrowing means that the qualifier essentially becomes unsafe in any ABI facing position, or any environment where the obt semantics can't be used everywhere. That would include the linux kernel due to module interfaces at least, as binary modules would not match the abi - i recall the linux kernel using macros to do/wrap things similar decltype but that was more than a decade ago.

Firstly, I really believe it would be entirely warranted to reject expressions with mismatching signedness if any are obt qualified.

Beyond that change I really believe that the safe semantics are one of:

  1. Reject any mismatch in underlying types. This would mean (char __obt_..)0 + 1 would be required to be a compile time error, or you get the kinds of errors and incompatibilities I've discussed previously. This removes many of the narrowing hazards, but retaining the smaller than int types for those expressions still retains all the previous problems discussed for int and wider types.
  2. Match existing promotion rules, propagating the obt semantics through those promotions
  • Any conversion that involves an obt qualified type the truncates the range of any value would apply the specified semantics if such truncation occurs (trapping, wrapping)
  • Equivalently casts (explicit or implicit) or assignment to storage with a mismatched/narrower type (if either the source or destination type is obt qualifier) would propagate and enforce the obt semantics

I think (1) is still problematic due to the smaller than int narrowing behaviour, e.g u8 __obt_wrap a = 255; u32 __obt_wrap b = a + 1 will produce b == 0 if obt is enabled, whereas it produces b == 256 if not, meanwhile u8 __obt_trap a = 255; u32 __obt_trap b = a + 1) - 1; now traps. Consider something like a buffer joining operation:

template<class T> struct Buffer {
  unsigned size; // type and qualifier not relevant here
  using data_type_t = T;
  data_type_t *data;
  std::span<data_type_t> span() { return std::span(data, size); }
};

// Again these types may not be specified explicitly, but aquired via macros,
// typedefs, template deduction, etc
template <class In> auto zip(auto f, std::span<In> a, std::span<In> b) -> std::vector<decltype(f(a[0], b[0]))> {
  std::vector<decltype(f(a[0], b[0]))> result;
  for (size_t i = 0; i < b.size(); i++) // Sssshhhh, assume a and b are the same size :D
    result.push_back(f(a[i], b[i]));
  return result;
}

void f(Buffer<uint8_t> buffer1, Buffer<uint8_t> buffer2) {
  ...
  auto v = zip([](auto a, auto b){ return a + b; }, buffer1.span(), buffer2.span());
  ...
}

At this point this produces a std::vector<int> that does not lose any data. Now imagine someone comes along and says "signed overflow is ub, so lets be safe and change data_type_t to T __obt_wrap in case T is a signed type" (imagine logic exists that only adds this qualifier if not already specified) - now the result type has changed from vector<int> to vector<uint8_t __obt_wrap>, and results are incorrect, and subsequent use of the vector will also truncate those subsequent results as well.

__obt_trap isn't much better - the intermediate calculations now trap instead of producing the expected output.

It's unclear if even a warning for passing/storing values of type narrower_type __obt_x to wider_type_t would be sufficient, because that doesn't work for deduced types, it doesn't work for non stored results (arguments to switch, if, etc). Even if we consider that sufficient that warning would occur a lot.

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Sep 29, 2025

Either always returning false (wrapping to uint8_t), or trapping in the overflow (?) check due to early narrowing, or returning false incorrectly but only if obt is enabled, and if obt is not enabled the only overflow exists on the promoted, but that can be prevented with:

uint8_t __obt_trap_wrapper u;
if (__UINT32_T_MAX__ - u < some_other_value)
  return;
if (u + some_other_value > 0xff) {

}

But now even that overflow test is incorrect, no matter how or when the truncation happens: the overflow test now traps in some cases and produces incorrect false results in others.

@ojhunt For what it's worth, check out #100272 and #104889 and #105709 (all related to Overflow Pattern Exclusions) -fsanitize-undefined-ignore-overflow-pattern=. In some instances, we can tinker with instrumentation for common overflow idioms, even trumping OBT annotations.

Anyways, it is clear we cannot move forward with any of the current non-traditional narrowing rules. The question is where do we go from here? @rjmccall suggested some models in the RFC recently and @ojhunt has suggested more traditional and more strict options as well. My estimate from reading everyone's feedback is that there are maybe 3 or 4 different approaches.

Of the approaches, most suggest that OBTs should behave more like regular arithmetic. Certain models mentioned by @kees and @ojhunt (and endorsed by @ojeda) introduce strict typing -- involving manual casts to strip or add behavior; a rust-style approach. Everything must be the same bitwidth, signed ness and OBT kind. This particular strictness seems hard to adopt in the Linux kernel but removes 99% of mathematical ambiguity. Other models (like wraps attribute feature I tried to land 1.5 years ago) involve persisting OBT kind through the usual arithmetic conversions and using them to trap on truncation.

From what I've read, most agree that wrap and trap should have the same semantics regardless of specific arithmetic model. However, I think there is some merit in considering different behaviors between wrap and trap. We could enforce a strict model, similar to what I mentioned above, for trap types and a more relaxed model for wrap types. I am also not opposed to some __ob_strict_trap or something like that, this allows for easier adoption from mature codebases.

We should schedule a meeting, when does the Clang Area Team meet next? Can we hijack part of that meeting? @AaronBallman

@ojhunt
Copy link
Contributor

ojhunt commented Sep 29, 2025

@ojhunt For what it's worth, check out #100272 and #104889 and #105709 (all related to Overflow Pattern Exclusions) -fsanitize-undefined-ignore-overflow-pattern=. In some instances, we can tinker with instrumentation for common overflow idioms, even trumping OBT annotations.

Yeah, many of these idioms are "UB" despite clearly having well defined behavior on every real platform, but I see the general idea of this feature being to aid these cases, but due to other browsers we may want to warn on those patterns regardless :-/

Anyways, it is clear we cannot move forward with any of the current non-traditional narrowing rules. The question is where do we go from here? @rjmccall suggested some models in the RFC recently and @ojhunt has suggested more traditional and more strict options as well. My estimate from reading everyone's feedback is that there are maybe 3 or 4 different approaches.

I think honestly both solutions are worthwhile independently: there are numerous cases where you want specific types to behavior deterministically in ways that are not UB, don't trigger sanitizers, etc.

But there are also places where you have a sequence of code where you know overflows are not expected anywhere, and overflow is bad - e.g scoped specification of overflow semantics is separately useful.

If both mechanisms are present at the same time with different semantics (say a scoped "trap on overflow", and a "wrap on overflow" variable might require a warning/error?)

Of the approaches, most suggest that OBTs should behave more like regular arithmetic.

I think that this is the only way to make this feature safe and adoptable - you can't have different semantics (beyond the UB removal) of unrelated behavior due to differing compilers (different compilers entirely, or different versions, that don't have this feature).

Certain models mentioned by @kees and @ojhunt (and endorsed by @ojeda) introduce strict typing -- involving manual casts to strip or add behavior; a rust-style approach. Everything must be the same bitwidth, signed ness and OBT kind. This particular strictness seems hard to adopt in the Linux kernel but removes 99% of mathematical ambiguity. Other models (like wraps attribute feature I tried to land 1.5 years ago) involve persisting OBT kind through the usual arithmetic conversions and using them to trap on truncation.

I think propagating the obt semantics is what would be what developers expect, but that might simply be projection of what I would expect. Basically I don't think anyone would expect an operation on an obt qualified type and a non-qualified type to discard that obt qualifier, and requiring explicit casts would be very verbose. Similarly I suspect mismatched size warnings/errors would be too noisy to be useful.

That said I think the signedness mismatch is reasonably worth making an error - I think there are already warning modes that complain about such behavior without obt available, so this seems reasonable.

From what I've read, most agree that wrap and trap should have the same semantics regardless of specific arithmetic model. However, I think there is some merit in considering different behaviors between wrap and trap. We could enforce a strict model, similar to what I mentioned above, for trap types and a more relaxed model for wrap types. I am also not opposed to some __ob_strict_trap or something like that, this allows for easier adoption from mature codebases.

I disagree (edit: disagree with strict_trap being useful) - again there's a fundamental problem that the existence of these qualifiers will be necessarily conditional, and mismatching behavior renders it unusable in practice. You could even hypothetically imagine someone having

#if /* debug or fuzzing build */
#define overflow_qualifier obt_strict_trap
#else
#define overflow_qualifier
#129159 

Which would change behavior between the modes in security relevant ways - e.g. obt_strict_trap would not be usable in practice, so is just additional complexity.

We should schedule a meeting, when does the Clang Area Team meet next? Can we hijack part of that meeting? @AaronBallman

Good idea - there's also the dev meeting soon if people are present (though holy heck that's expensive O_o)

(I think I successfully avoided wall of text!!!! \o/)

@efriedma-quic
Copy link
Collaborator

As the clang area team, we generally prefer to schedule specific meetings for RFC discussions, so we can pick a timeslot that works for the people we expect to attend. If you think you need a meeting, please send an email to me and @rnk with the topic of discussion, who's leading the discussion (that person should prepare a few slides in advance), and a list of a few high-priority attendees.

(The clang area team does have biweekly meetings, but we don't have time for deep technical discussions in that timeslot.)

That said, looking over the discussion, I'm not sure anyone is advocating for a specific solution at the moment. You might want to just reach out directly to a couple people for a brainstorming session.

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Sep 30, 2025

With all the review it is clear that we have to choose some new semantics for OBTs. It is also important that OBTs are useful in their design and purpose for many projects. OBTs should provide type-level overflow behavior handling. With this goal in mind, there's two customers: 1) large existing codebases and 2) new projects.

To help all kinds of projects I met with @kees again and we talked about separating some behaviors into separate modes. We identified two modes that would give the best path forward for the Linux kernel, other existing projects, and new projects. We were thinking of a "compliant" mode and a "strict" mode. Name bike-shedding welcome. :)

@ojhunt We see your concerns with a strict mode and want to address them by ensuring code compatibility between all modes. Further below are some examples and design principles for the modes. First, let's make the distinction clear:

The "compliant" mode would use traditional C promotion rules with the exception that the OBT qualifier is persisted through implicit casts. This allows us to get truncation signal during storage of less-than-int arithmetic results and overflow signal on other results. This mode would be the introductory mode for large projects that cannot make the direct jump to strict mode. @kees has shown that this compliant mode would still provide useful signal in the Linux kernel, where truncation accounts for a large percentage of the integer overflow flaws.

An example of compliant mode:

typedef unsigned short __ob_trap tu16;

tu16 a = 65535; // USHORT_MAX
int b = 1;
tu16 c = a + b; // a and b promoted to "__ob_trap int", traps on truncated assignment

// a+b is implicitly promoted to __ob_trap int
// result of a=b is 65536
// 65536 doesn't fit within tu16's storage space, so we can trap on the assignment

The "strict" mode would require matching bitwidths and obt kinds which results in no ambiguity and provides homogeneity of arithmetic results to gain full visibility into potential overflows. For example, this matches the semantics of Rust. Nothing implicit happens in this mode. To make this mode most useful, it would need that casts to strict obt kinds get instrumented for overflow (more on this below).

Same example with strict mode:

typedef unsigned short __ob_strict_trap tu16;

tu16 a = 65535; // USHORT_MAX
int b = some();
tu16 c = a + b; // error: a and b have different types

solution 1:
// Make sure everything has the same type
tu16 a = 65535;
tu16 b = some(); // some() must also return tu16.
tu16 c = a + b; // OK, everything is the same type. 

solution 2:
// add explicit casts
tu16 a = 65535;
int b = some();
tu16 c = a + (tu16)b; // OK, everything is the same type...
// ... but we must avoid potential silent dataloss during c-style cast.

To make the "strict" mode usable, it would also necessitate the need for c-style casts to be instrumentable, otherwise we risk silent truncations. This cast instrumentation would be used to get full signal across arithmetic expressions being converted from compliant to strict mode.

Take a look at this toy example case which shows the usefulness of strict obts, which catches the other major class of integer overflow the Linux kernel wants to catch:

// vanilla C with -fwrapv (i.e. Linux kernel today)
int x = INT_MAX;
int y = (INT_MAX-99);
u8 sz = x * y; // sz is 100 due to wrap-around, no truncation
... = malloc(sz); // buggy malloc of 100 bytes

// compliant trap mode (would not catch this kind of overflow)
typedef unsigned char __ob_trap tu8;

int x = INT_MAX;
int y = (INT_MAX-99);
tu8 sz = x * y; // sz is 100 due to wrap-around, no truncation so no trap
... = malloc(sz); // buggy malloc 100 bytes

// strict trap mode
typedef unsigned char __ob_strict_trap tu8;

int x = INT_MAX;
int y = (INT_MAX-99);
tu8 sz = (tu8)x * (tu8)y; // strict mode forces same types, so add casts
// The casts will add instrumentation to catch data loss at runtime.
... = malloc(sz);

// When using strict obts the ultimate goal is to have code changed to all
// matching types instead of littering casts everywhere

// So, imagine a more opaque example with some() and other()
tu8 x = some(); // refactor these apis to use tu8
tu8 y = other(); 
tu8 sz = x * y; // now all types are the same with no casts locally.
... = malloc(sz);

Refactoring to use __ob_strict_trap is still safe/stable when obts are unsupported, because the casts don't make anything worse. If, instead, we added optional bit-width/kind mismatch warnings to the "compliant" mode, we run the risk of bad casts (e.g. just "u8" above) being added, which would silently hide the overflow and silence the bit-width warning.

It's also possible that we just need a stand-alone "strict" type qualifier that requires annotated types cannot participate in any implicit promotions. And this could then just be applied to the existing obts (or any other types).

So the process for an existing project would be to migrate some types (e.g. size_t) via the compliant obts, and in other places (e.g. new types, new code, APIs, etc), use the strict obts. This should provide the greatest flexibility without compromising on coverage. For example:

#if __has_attribute(overflow_behavior)
// "compliant" obt for an "existing" type
typedef unsigned long __ob_trap size_t;
// "strict" obt for a "new" type
typedef unsigned char __strict __ob_trap tu8;
#else
typedef unsinged long size_t;
typedef unsigned char tu8;
#endif

It is important that strict mode doesn't carry different conversion semantics. It may only enforce stricter type rules requring explicit casts or type changes.

typedef unsigned char __strict __ob_trap tu8;

extern void some(int);

void foo(int x, int y) {
  // must get 'x' and 'y' to be of type 'tu8'
  // old compilers will build this code just fine (and trap on truncation)
  // obt-enabled compilers will fail to build as there are mismatching types
  tu8 a = x * y;
  some(a);
}

... Now convert the code to build with 'strict' mode.

void foo(int x, int y) {
  // old compiler will still build this just fine
  // obt-enabled compilers will now build (and instrument explicit casts for data loss)
  // no difference in result between compilers
  tu8 a = (tu8)x * (tu8)y;
  some(a);
}

@rjmccall
Copy link
Contributor

I certainly understand that "strict mode" is a model that works well in other languages — we also use this model in Swift — but I don't think it makes any sense to introduce it as a global mode in C, because it's simply too different from the normal semantics of the language.

Having a __strict specifier is much more reasonable. It is essentially a "strong typedef", and you might consider looking up existing WG14/WG21 papers on that.

You linked me to this post promising a detailed discussion of the model, but I can't quite piece out what overflow model you're actually proposing for the compliant mode. :)

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Sep 30, 2025

@rjmccall

You linked me to this post promising a detailed discussion of the model, but I can't quite piece out what overflow model you're actually proposing for the compliant mode. :)

Sorry, I tried to fit all the information in my post without writing a book. To summarize, the model I propose for "compliant" mode best matches Model 2 from your comment on the RFC.

The usual arithmetic conversions take place normally and we bubble up any obt qualifiers through implicit casts. Most of the time we can catch trivial overflow with int-or-greater types like

int __ob_trap a = INT_MAX;
(a+1); // trap!

but for less-than-int cases we can instrument the assignment or storage to trap on truncation:

u8 __ob_trap a = 255;
u8 __ob_trap b = a + 1; // trap! (on assignment truncation)

// (a+1) is brought up to an `__ob_trap int` as per usual arithmetic conversions.
// that doesn't fit into u8 without data loss. so trap.

@rjmccall
Copy link
Contributor

rjmccall commented Sep 30, 2025

Thanks. So IIUC, the rule is:

  1. If the type of either operand of an arithmetic operator is __ob_trap, the operator traps on overflow, and the result type is __ob_trap.
  2. If either the operand type or the destination type of an integer conversion is __ob_trap, the conversion traps on overflow.

If that's right, that seems pretty reasonable. I still favor a scope-based approach, but this is a substantially improved type-based approach from how it was before.

@kees
Copy link
Contributor

kees commented Oct 1, 2025

Would it be reasonable to split this into two phases?

  1. this PR, but with standard promotion semantics and instrumented casting.

  2. new PR that adds a type qualifier named __strict or __no_implicit_promotion that forces annotated types to not be involved in implicit promotions.

@JustinStitt
Copy link
Contributor Author

Would it be reasonable to split this into two phases?

I think two phases is a good idea. This lets us get OBTs sooner and design __strict separately.

Does anyone know WG14s consensus over strong typedefs? __strict is similar enough that I'm curious to know what folks thought about them.

@rjmccall
Copy link
Contributor

rjmccall commented Oct 1, 2025

There's a proposal which the committee has discussed briefly. It's not at a point where I'd feel comfortable predicting what the committee will ultimately decide about it.

@ojeda
Copy link

ojeda commented Oct 1, 2025

Agreed, some people in the committee wanted/want a feature like that, but it is hard to say what form it would take, if any.

@JustinStitt
Copy link
Contributor Author

JustinStitt commented Oct 2, 2025

OK, I'll work on scaling back my custom promotion/conversion rules. Might take a couple days because every test needs to be rewritten too.

I'll also work on a __strict prototype.

@JustinStitt JustinStitt force-pushed the overflow-behavior-types-pr branch from 373d622 to cdc7493 Compare October 7, 2025 00:01
@JustinStitt
Copy link
Contributor Author

I rewrote OBT conversion rules to adhere to the spec, with the exception that obt behaviors persist through implicit conversions. I've updated the docs and the tests then squashed everything back to a single commit.

If you've reviewed this PR before, I highly recommend re-reading the docs.

Hopefully this implementation is going more in the right direction, looking forward to feedback :)

CC'ing folks who sent a post in the past week or so: @kees @ojeda @rjmccall @ojhunt @efriedma-quic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:as-a-library libclang and C++ API clang:codegen IR generation bugs: mangling, exceptions, etc. clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category debuginfo lldb
Projects
None yet
Development

Successfully merging this pull request may close these issues.